Rust MaybeUninit

Union MaybeUninit is a wrapper type to construct uninitialized instances of T.

pub union MaybeUninit {
    uninit: (),
    value: ManuallyDrop,
}

MaybeUninit is providing a safe, controlled way to work with uninitialized memory.

Like the [Point: 10] in the following example, it is redundant to initialize all the Points to 0.0 at the beginning of the function create_points_with_init, we may want to avoid the redundant initialization, and set each point one-by-one directly.

use std::mem::MaybeUninit;

#[derive(Debug, Clone, Copy)]
struct Point {
    x: f32,
    y: f32,
    z: f32,
}

fn create_points_with_init() -> [Point; 10] {
    // the redundant initialization
    let mut points: [Point; 10] = [Point {
        x: 0.0,
        y: 0.0,
        z: 0.0,
    }; 10];

    for (i, point) in points.iter_mut().enumerate() {
        point.x = i as f32;
        point.y = (i * 2) as f32;
        point.z = (i * 3) as f32;
    }

    points
}

We can simply construct an uninitialized instance of [Piont; 10]: MaybeUninit<[Point; 10]> to skip the initialization.

fn create_points_without_init() -> [Point; 10] {
    let mut points: MaybeUninit<[Point; 10]> = MaybeUninit::uninit();

    let ptr = points.as_mut_ptr() as *mut Point;

    for i in 0..10 {
        unsafe {
            ptr.add(i).write(Point {
                x: i as f32,
                y: (i * 2) as f32,
                z: (i * 3) as f32,
            });
        }
    }
    unsafe { points.assume_init() }
}

fn main() {
    println!("create points with init");
    let points = create_points_without_init();
    for point in points {
        println!("{point:?}");
    }

    println!("");
    println!("create points without init");
    let points = create_points_with_init();
    for point in points {
        println!("{point:?}");
    }
}

Undefined Behavior

It will cause UB (undefined behavior) when trying to extract value from uninitialized container.

fn main() {
    let nums: [usize; 10] = unsafe { MaybeUninit::uninit().assume_init() };
    println!("{nums:?}");
}

cargo run can be executed normally, but the values of nums cannot be determined.

And we can use miri to detect UB

❯  cargo +nightly miri run

error: Undefined Behavior: constructing invalid value at .value[0]: encountered uninitialized memory, but expected an integer
  --> src/main.rs:58:38
   |
58 |     let nums: [usize; 10] = unsafe { MaybeUninit::uninit().assume_init() };
   |                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ constructing invalid value at .value[0]: encountered uninitialized memory, but expected an integer

The right way is let the compiler know each item in the array is uninitialized.

let nums: [MaybeUninit; 10] = unsafe { MaybeUninit::uninit().assume_init() };

Drop

Note that dropping a MaybeUninit will never call T’s drop code.

Take an example below, it's OK not to call assume_init_drop for num: MaybeUninit and point: MaybeUninit. But we must call it for string: MaybeUninit, otherwise it will cause error "memory leaked". As String implement trait Drop, and it need to call drop function to clean the resource on the heap that it owns.

let mut num: MaybeUninit = MaybeUninit::uninit();
unsafe { num.assume_init_drop() };
num.write(100);
// unsafe { num.assume_init_drop() };

let mut point: MaybeUninit = MaybeUninit::uninit();
point.write(Point {
	x: 0.0,
	y: 0.1,
	z: 0.2,
});
// unsafe { point.assume_init_drop() };

let mut string: MaybeUninit = MaybeUninit::uninit();
string.write("string".into());
// error: memory leaked, if not call String's drop
// unsafe { string.assume_init() };
// unsafe { string.assume_init_drop() };

Copy types ​​never need assume_init_drop()​ because they have no destructor logic. Copy types are ​​prohibited from implementing Drop​ (language guarantee)